﻿// Copyright (c) 2013, Simon Carter.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted 
// provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this list of 
//   conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice, this list of 
//  conditions and the following disclaimer in the documentation and/or other materials provided 
//  with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// History
// Initial Release - Simon Carter
//

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Management;

using TCPMessageServer;
using ErrorManager.ErrorClient.Forms;


namespace ErrorManager.ErrorClient
{
    /// <summary>
    /// Client Error Handling Class
    /// </summary>
    public sealed class ErrorClient
    {
        #region Private Members

        private static ErrorClient _errorClient;

        private string _sysLog;
        private ProcessStage _stage;
        private Options _options;
        private DisplayOptions _displayOptions;
        private bool _web = false;

        #endregion Private Members

        #region Static Methods

        /// <summary>
        /// Initialises the ErrorClient
        /// 
        /// Only used to automate error handling, will automatically hook up to Global Excption Handlers.
        /// </summary>
        /// <param name="displayOptions">Display Options to be used.</param>
        /// <param name="options">Connection Options to connect to the server.</param>
        public static void InitErrorClient(DisplayOptions displayOptions, Options options)
        {
            if (displayOptions == null)
                throw new ArgumentNullException("displayOptions");

            if (options == null)
                throw new ArgumentNullException("options");

            if (_errorClient == null)
            {
                _errorClient = new ErrorClient();

                AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(_errorClient.CurrentDomain_UnhandledException);
                System.Windows.Forms.Application.ThreadException += new ThreadExceptionEventHandler(_errorClient.Application_ThreadException);

                _errorClient._options = options;
                _errorClient._displayOptions = displayOptions;
                _errorClient._web = false;
            }
        }

        public static void InitWebErrorClient(Options options)
        {
            if (options == null)
                throw new ArgumentNullException("options");

            if (_errorClient == null)
            {
                _errorClient = new ErrorClient();

                AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(_errorClient.CurrentDomain_UnhandledException);
                System.Windows.Forms.Application.ThreadException += new ThreadExceptionEventHandler(_errorClient.Application_ThreadException);

                _errorClient._options = options;
                _errorClient._web = true;
            }
        }

        #endregion Static Methods

        #region Static Properties

        /// <summary>
        /// Returns the active ErrorClient instance
        /// </summary>
        public static ErrorClient GetErrorClient
        {
            get
            {
                if (_errorClient == null)
                    throw new InvalidOperationException("InitErrorClient() not called");

                return (_errorClient);
            }
        }

        #endregion Static Properties

        #region Public Methods

        /// <summary>
        /// Sends Web Error information to the server
        /// 
        /// ASP.Net only!
        /// </summary>
        /// <param name="webException">Exception that occured</param>
        /// <param name="sessionID">Local Session ID</param>
        /// <param name="url">URL of file where error occured</param>
        /// <param name="physicalPath">File where error occured</param>
        /// <returns></returns>
        public bool SendWebError(Exception webException, string sessionID, string url, string physicalPath)
        {
            string Inner = "Unknown";
            string Message = "Unknown";
            string Source = "Unknown";
            string StackTrace = "Unknown";
            string TargetSite = "Unknown";

            if (webException != null)
            {
                Inner = webException.InnerException == null ? "InnerException is null" : webException.InnerException.ToString();
                Message = webException.Message;
                Source = webException.Source;
                StackTrace = webException.StackTrace;
                TargetSite = webException.TargetSite.ToString();
            }

            string Msg = String.Format("User Session: {5}\r\n\r\nURL: {6}\r\n\r\n"+
                "Physical Path: {7}\r\n\r\nError Message: {0}\r\n" +
                "\r\nInner Exception: {1}\r\n\r\nSource: {2}\r\n" +
                "\r\nStackTrace: {3}\r\n\r\nTarget Site: {4}\r\n",
                Message, Inner, Source, StackTrace, TargetSite, sessionID, url, physicalPath);

            return (SendError(_options, Msg));
        }

        /// <summary>
        /// Sends Error Information to the Server
        /// 
        /// WinForms only!
        /// </summary>
        /// <param name="options">User options</param>
        /// <param name="ErrorInformation">String containing all error information</param>
        /// <param name="ExtraInformation">String of extra information to be sent to the server</param>
        /// <returns>bool true if data succesfully sent, otherwise false</returns>
        public bool SendError(Options Options, string ErrorInformation)
        {
            _options = Options;
            RaiseProgressEvent(ProcessStage.Initialising);
            bool Result = false;

            if (Options.IncludeSystemInformation)
                BuildSystemInformation();
            else
                _sysLog = String.Empty;

            MessageClient client = new MessageClient(Options.Host, Options.Port);
            try
            {
                client.ClientLogin += client_ClientLogin;
                client.ClientLoginFailed += client_ClientLoginFailed;
                client.ClientLoginSuccess += client_ClientLoginSuccess;
                client.OnError += client_OnError;
                client.MessageReceived += client_MessageReceived;
                client.FileReceive += client_FileReceive;
                client.FileReceived += client_FileReceived;
                client.ConnectionRefused += client_ConnectionRefused;
                client.ClientIDChanged += client_ClientIDChanged;

                RaiseProgressEvent(ProcessStage.ContactingServer, Options.Host);

                client.StartListening();

                DateTime timeout = DateTime.Now;
                TimeSpan span = DateTime.Now - timeout;

                while (client.ClientID == null || !client.LoggedIn)
                {

                    System.Threading.Thread.Sleep(100);
                    span = DateTime.Now - timeout;

                    if (span.TotalSeconds > Options.ConnectTimeout)
                        break;

                    // check if failed login / refused connection
                    switch (_stage)
                    {
                        case ProcessStage.ConnectionRefused:
                        case ProcessStage.LoginFailed:
                            return (false);

                    }

                    RaiseProgressEvent(ProcessStage.Waiting);
                }

                if (span.Seconds > Options.ConnectTimeout)
                {
                    RaiseProgressEvent(ProcessStage.TimeOut);
                }
                else
                {
                    client.BufferSize = FileBufferSize.Size16384;
                    RaiseProgressEvent(ProcessStage.SendingErrorInformation);

                    client.SendMessage(new Message("Error Information", ErrorInformation, MessageType.Error));

                    if (Options.IncludeSystemInformation)
                    {
                        RaiseProgressEvent(ProcessStage.SendingSystemInfo);
                        client.SendMessage(new Message("System Information", _sysLog, MessageType.Error));
                    }

                    //send extra information
                    string extraInformation = RaiseAdditionalInformation();
                    if (!String.IsNullOrEmpty(extraInformation))
                    {
                        RaiseProgressEvent(ProcessStage.SendingExtraInformation);
                        client.SendMessage(new Message("Extra Information", extraInformation, MessageType.Error));
                    }

                    //Additional Files
                    List<string> extraFiles = RaiseAdditionalFiles();

                    foreach (string extraFile in extraFiles)
                    {
                        RaiseProgressEvent(ProcessStage.SendingFile, extraFile);
                        client.SendFile(extraFile);

                        timeout = DateTime.Now;

                        while (_stage == ProcessStage.SendingFile)
                        {
                            System.Threading.Thread.Sleep(100);
                            span = DateTime.Now - timeout;

                            if (span.TotalSeconds > Options.FileSendTimeout)
                                break;

                            RaiseProgressEvent(ProcessStage.Waiting);
                        }

                        if (span.Seconds > Options.FileSendTimeout)
                        {
                            RaiseProgressEvent(ProcessStage.TimeOut);
                            break;
                        }
                    }

                }

                RaiseProgressEvent(ProcessStage.ConfirmingReceiptOfData);
                client.SendMessage(new Message("Complete", String.Empty, MessageType.Acknowledge));
                client.SendMessage(new Message(Options.Username, "A new error has just been submitted.", MessageType.User));
                while (_stage == ProcessStage.ConfirmingReceiptOfData)
                {
                    System.Threading.Thread.Sleep(100);
                }

                RaiseProgressEvent(ProcessStage.Disconnecting);
                client.StopListening();

                while (client.IsConnected)
                    System.Threading.Thread.Sleep(100);

                Result = true;
            }
            catch (Exception err)
            {
                RaiseProgressEvent(ProcessStage.Error, err.Message);
            }

            RaiseProgressEvent(ProcessStage.Complete, Result ? "Data sent to server" : "Failed to send data to server");

            return (Result);
        }

        #endregion Public Methods

        #region Private Methods

        private void BuildSystemInformation()
        {
            RaiseProgressEvent(ProcessStage.BuildingSystemInformation);

            InsertSystemInfo("Win32_Processor");
            InsertSystemInfo("Win32_Printer");
            InsertSystemInfo("Win32_SMBIOSMemory");
            InsertSystemInfo("Win32_SoundDevice");
            InsertSystemInfo("Win32_SCSIController");
            InsertSystemInfo("Win32_DiskDrive");
            InsertSystemInfo("Win32_LogicalDisk");
            InsertSystemInfo("Win32_PhysicalMemory");
            InsertSystemInfo("Win32_Process");
            InsertSystemInfo("Win32_ComputerSystem");
            InsertSystemInfo("Win32_Service");
            InsertSystemInfo("Win32_NetworkAdapter");
            InsertSystemInfo("Win32_DisplayConfiguration");
            InsertSystemInfo("Win32_Keyboard");
            InsertSystemInfo("Win32_MotherboardDevice");
            InsertSystemInfo("Win32_OperatingSystem");
        }

        void client_ClientIDChanged(object sender, EventArgs e)
        {

        }

        void client_ConnectionRefused(object sender, EventArgs e)
        {
            RaiseProgressEvent(ProcessStage.ConnectionRefused);
        }

        void client_FileReceived(object sender, TransferFileEventArgs e)
        {
            if (e.Total == e.Received)
                RaiseProgressEvent(ProcessStage.FileSent, e.FileName);
        }

        void client_FileReceive(object sender, TransferFileEventArgs e)
        {
            if (e.Total == e.Received)
                RaiseProgressEvent(ProcessStage.FileSent, e.FileName);
        }

        void client_MessageReceived(object sender, Message message)
        {
            if (message.Type == MessageType.Acknowledge && _stage == ProcessStage.ConfirmingReceiptOfData)
                _stage = ProcessStage.ReceiptConfirmed;
        }

        void client_OnError(object sender, ErrorEventArgs e)
        {

            RaiseProgressEvent(ProcessStage.Error, e.Error.Message);

        }

        void client_ClientLoginSuccess(object sender, EventArgs e)
        {
            RaiseProgressEvent(ProcessStage.LoggedIn);
        }

        void client_ClientLoginFailed(object sender, EventArgs e)
        {
            RaiseProgressEvent(ProcessStage.LoginFailed);
        }

        void client_ClientLogin(object sender, ClientLoginArgs e)
        {
            e.Username = _options.Username;
            e.Password = _options.Password;
            RaiseProgressEvent(ProcessStage.LoggingIn);
        }

        private void AddToSysLog(string Share, string Property, string Data)
        {
            _sysLog += String.Format("{0} - {1} : {2}\r\n", Share, Property, Data);
        }

        private void InsertSystemInfo(string Key)
        {
            AddToSysLog("\r\n\r\n", String.Empty, String.Empty);

            RaiseProgressEvent(ProcessStage.BuildingSystemInformation, Key);

            ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from " + Key);

            try
            {
                foreach (ManagementObject share in searcher.Get())
                {
                    if (share.Properties.Count <= 0)
                    {
                        AddToSysLog(String.Format("No Information Available for {0}", Key), String.Empty, String.Empty);
                        return;
                    }

                    foreach (PropertyData PC in share.Properties)
                    {
                        if (PC.Value != null && PC.Value.ToString() != "")
                        {
                            switch (PC.Value.GetType().ToString())
                            {
                                case "System.String[]":
                                    string[] str = (string[])PC.Value;

                                    string str2 = "";
                                    foreach (string st in str)
                                        str2 += st + " ";

                                    AddToSysLog(share["Name"].ToString(), PC.Name, str2);

                                    break;
                                case "System.UInt16[]":
                                    ushort[] shortData = (ushort[])PC.Value;

                                    string tstr2 = "";
                                    foreach (ushort st in shortData)
                                        tstr2 += st.ToString() + " ";

                                    AddToSysLog(share["Name"].ToString(), PC.Name, tstr2);

                                    break;
                                default:
                                    AddToSysLog(share["Name"].ToString(), PC.Name, PC.Value.ToString());

                                    break;
                            }
                        }
                        else
                        {
                            continue;
                        }
                    }
                }
            }
            catch (Exception exp)
            {
                AddToSysLog(String.Format("can't get {0} data because of the followeing error {1}", Key, exp.Message), String.Empty, String.Empty);
            }

            AddToSysLog("\r\n\r\n", String.Empty, String.Empty);
        }

        #endregion Private Methods

        #region Global Exception Handling

        private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            try
            {
                Exception ex = (Exception)e.ExceptionObject;

                if (!ErrorHandlerForm.DisplayErrorDetails(GetErrorClient._displayOptions, GetErrorClient._options, ex))
                    System.Windows.Forms.Application.Exit();
            }
            catch (Exception err)
            {
                string s = err.Message;
                System.Windows.Forms.Application.Exit();
            }
        }

        private void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
        {
            try
            {
                Exception ex = (Exception)e.Exception;

                if (!ErrorHandlerForm.DisplayErrorDetails(GetErrorClient._displayOptions, GetErrorClient._options, ex))
                    System.Windows.Forms.Application.Exit();
            }
            catch
            {
                System.Windows.Forms.Application.Exit();
            }
        }

        #endregion Global Exception Handling

        #region Events

        /// <summary>
        /// Raises OnProgress Event
        /// </summary>
        /// <param name="Stage">Current Process Stage</param>
        /// <param name="Information">Extra Information relevant to the Stage</param>
        private void RaiseProgressEvent(ProcessStage Stage, string Information = "")
        {
            if (Stage != ProcessStage.Waiting)
                _stage = Stage;

            if (OnProgress != null)
                OnProgress(this, new ProgressEventArgs(Stage, Information));
        }

        private List<string> RaiseAdditionalFiles()
        {
            AdditionalFilesEventArgs args = new AdditionalFilesEventArgs();

            if (AdditionalFiles != null)
                AdditionalFiles(this, args);

            return (args.Files);
        }

        private string RaiseAdditionalInformation()
        {
            AdditionalInformationEventArgs args = new AdditionalInformationEventArgs();

            if (AdditionalInformation != null)
                AdditionalInformation(this, args);

            return (args.Information);
        }

        /// <summary>
        /// Event raised to indicate progress when sending error information
        /// </summary>
        public event ProgressHandler OnProgress;

        /// <summary>
        /// Event raised to retrieve additional files to be sent to the Error Server
        /// </summary>
        public event AdditionalFilesHandler AdditionalFiles;

        /// <summary>
        /// Event raised to retrieve additional information to be sent to the Error Server
        /// </summary>
        public event AdditionalInformationHandler AdditionalInformation;

        #endregion Events
    }
}
